Selami sistem injeksi dependensi FastAPI yang canggih. Pelajari teknik lanjutan, dependensi khusus, cakupan, dan strategi pengujian untuk pengembangan API yang kuat.
Sistem Dependensi FastAPI: Injeksi Dependensi Tingkat Lanjut
Sistem injeksi dependensi (DI) FastAPI adalah landasan desainnya, mempromosikan modularitas, kemampuan pengujian, dan penggunaan kembali. Sementara penggunaan dasar sangat mudah, menguasai teknik DI tingkat lanjut membuka kekuatan dan fleksibilitas yang signifikan. Artikel ini membahas injeksi dependensi tingkat lanjut di FastAPI, yang mencakup dependensi khusus, cakupan, strategi pengujian, dan praktik terbaik.
Memahami Dasar-Dasarnya
Sebelum menyelami topik tingkat lanjut, mari kita rekap secara singkat dasar-dasar injeksi dependensi FastAPI:
- Dependensi sebagai Fungsi: Dependensi dideklarasikan sebagai fungsi Python biasa.
- Injeksi Otomatis: FastAPI secara otomatis menyuntikkan dependensi ini ke dalam operasi jalur berdasarkan petunjuk tipe.
- Petunjuk Tipe sebagai Kontrak: Petunjuk tipe menentukan tipe input yang diharapkan untuk dependensi dan fungsi operasi jalur.
- Dependensi Hierarkis: Dependensi dapat bergantung pada dependensi lain, membuat pohon dependensi.
Berikut adalah contoh sederhana:
from fastapi import FastAPI, Depends
app = FastAPI()
def get_db():
db = {"items": []}
try:
yield db
finally:
# Tutup koneksi jika diperlukan
pass
@app.get("/items/")
async def read_items(db: dict = Depends(get_db)):
return db["items"]
Dalam contoh ini, get_db adalah dependensi yang menyediakan koneksi database. FastAPI secara otomatis memanggil get_db dan menyuntikkan hasilnya ke dalam fungsi read_items.
Teknik Dependensi Tingkat Lanjut
1. Menggunakan Kelas sebagai Dependensi
Meskipun fungsi umumnya digunakan, kelas juga dapat berfungsi sebagai dependensi, memungkinkan pengelolaan status dan metode yang lebih kompleks. Ini sangat berguna saat berhadapan dengan koneksi database, layanan otentikasi, atau sumber daya lain yang memerlukan inisialisasi dan pembersihan.
from fastapi import FastAPI, Depends
app = FastAPI()
class Database:
def __init__(self):
self.connection = self.create_connection()
def create_connection(self):
# Simulasikan koneksi database
print("Membuat koneksi database...")
return {"items": []}
def close(self):
# Simulasikan menutup koneksi database
print("Menutup koneksi database...")
def get_db():
db = Database()
try:
yield db.connection
finally:
db.close()
@app.get("/items/")
async def read_items(db: dict = Depends(get_db)):
return db["items"]
Dalam contoh ini, kelas Database merangkum logika koneksi database. Dependensi get_db membuat instance kelas Database dan menghasilkan koneksi. Blok finally memastikan bahwa koneksi ditutup dengan benar setelah permintaan diproses.
2. Mengganti Dependensi
FastAPI memungkinkan Anda mengganti dependensi, yang sangat penting untuk pengujian dan pengembangan. Anda dapat mengganti dependensi nyata dengan mock atau stub untuk mengisolasi kode Anda dan memastikan hasil yang konsisten.
from fastapi import FastAPI, Depends
app = FastAPI()
# Dependensi asli
def get_settings():
# Simulasikan memuat pengaturan dari file atau lingkungan
return {"api_key": "real_api_key"}
@app.get("/items/")
async def read_items(settings: dict = Depends(get_settings)):
return {"api_key": settings["api_key"]}
# Timpa untuk pengujian
def get_settings_override():
return {"api_key": "test_api_key"}
app.dependency_overrides[get_settings] = get_settings_override
# Untuk kembali ke aslinya:
# del app.dependency_overrides[get_settings]
Dalam contoh ini, dependensi get_settings ditimpa dengan get_settings_override. Ini memungkinkan Anda menggunakan kunci API yang berbeda untuk tujuan pengujian.
3. Menggunakan `contextvars` untuk Data yang Tercakup dalam Permintaan
contextvars adalah modul Python yang menyediakan variabel lokal konteks. Ini berguna untuk menyimpan data spesifik permintaan, seperti informasi otentikasi pengguna, ID permintaan, atau data pelacakan. Menggunakan contextvars dengan injeksi dependensi FastAPI memungkinkan Anda mengakses data ini di seluruh aplikasi Anda.
import contextvars
from fastapi import FastAPI, Depends, Request
app = FastAPI()
# Buat variabel konteks untuk ID permintaan
request_id_var = contextvars.ContextVar("request_id")
# Middleware untuk mengatur ID permintaan
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = str(uuid.uuid4())
request_id_var.set(request_id)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
# Dependensi untuk mengakses ID permintaan
def get_request_id():
return request_id_var.get()
@app.get("/items/")
async def read_items(request_id: str = Depends(get_request_id)):
return {"request_id": request_id}
Dalam contoh ini, middleware menetapkan ID permintaan unik untuk setiap permintaan masuk. Dependensi get_request_id mengambil ID permintaan dari konteks contextvars. Ini memungkinkan Anda melacak permintaan di seluruh aplikasi Anda.
4. Dependensi Asinkron
FastAPI dengan mulus mendukung dependensi asinkron. Ini penting untuk operasi I/O non-pemblokiran, seperti kueri database atau panggilan API eksternal. Cukup definisikan fungsi dependensi Anda sebagai fungsi async def.
from fastapi import FastAPI, Depends
import asyncio
app = FastAPI()
async def get_data():
# Simulasikan operasi asinkron
await asyncio.sleep(1)
return {"message": "Halo dari dependensi asinkron!"}
@app.get("/items/")
async def read_items(data: dict = Depends(get_data)):
return data
Dalam contoh ini, dependensi get_data adalah fungsi asinkron yang mensimulasikan penundaan. FastAPI secara otomatis menunggu hasil dependensi asinkron sebelum menyuntikkannya ke dalam fungsi read_items.
5. Menggunakan Generator untuk Pengelolaan Sumber Daya (Koneksi Database, Handle File)
Menggunakan generator (dengan yield) menyediakan pengelolaan sumber daya otomatis, menjamin sumber daya ditutup/dilepaskan dengan benar melalui blok `finally` bahkan jika terjadi kesalahan.
from fastapi import FastAPI, Depends
app = FastAPI()
def get_file_handle():
try:
file_handle = open("my_file.txt", "r")
yield file_handle
finally:
file_handle.close()
@app.get("/file_content/")
async def read_file_content(file_handle = Depends(get_file_handle)):
content = file_handle.read()
return {"content": content}
Cakupan dan Siklus Hidup Dependensi
Memahami cakupan dependensi sangat penting untuk mengelola siklus hidup dependensi dan memastikan bahwa sumber daya dialokasikan dan dilepaskan dengan benar. FastAPI tidak secara langsung menawarkan anotasi cakupan eksplisit seperti beberapa kerangka kerja DI lainnya (misalnya, `@RequestScope` Spring, `@ApplicationScope`), tetapi kombinasi cara Anda mendefinisikan dependensi dan cara Anda mengelola status mencapai hasil yang serupa.
Cakupan Permintaan
Ini adalah cakupan yang paling umum. Setiap permintaan menerima instance dependensi yang baru. Ini biasanya dicapai dengan membuat objek baru di dalam fungsi dependensi dan menghasilkannya, seperti yang ditunjukkan dalam contoh Database sebelumnya. Menggunakan contextvars juga membantu mencapai cakupan permintaan.
Cakupan Aplikasi (Singleton)
Satu instance dependensi dibuat dan dibagikan di semua permintaan di seluruh siklus hidup aplikasi. Ini sering dilakukan menggunakan variabel global atau atribut tingkat kelas.
from fastapi import FastAPI, Depends
app = FastAPI()
# Instance Singleton
GLOBAL_SETTING = {"api_key": "global_api_key"}
def get_global_setting():
return GLOBAL_SETTING
@app.get("/items/")
async def read_items(setting: dict = Depends(get_global_setting)):
return setting
Berhati-hatilah saat menggunakan dependensi dengan cakupan aplikasi dengan status yang dapat berubah, karena perubahan yang dibuat oleh satu permintaan dapat memengaruhi permintaan lain. Mekanisme sinkronisasi (kunci, dll.) mungkin diperlukan jika aplikasi Anda memiliki permintaan bersamaan.
Cakupan Sesi (Data Spesifik Pengguna)
Kaitkan dependensi dengan sesi pengguna. Ini memerlukan mekanisme pengelolaan sesi (misalnya, menggunakan cookie atau JWT) dan biasanya melibatkan penyimpanan dependensi dalam data sesi.
from fastapi import FastAPI, Depends, Cookie
from typing import Optional
import uuid
app = FastAPI()
# Dalam aplikasi nyata, simpan sesi dalam database atau cache
sessions = {}
async def get_user_id(session_id: Optional[str] = Cookie(None)) -> str:
if session_id is None or session_id not in sessions:
session_id = str(uuid.uuid4())
sessions[session_id] = {"user_id": str(uuid.uuid4())} # Tetapkan ID pengguna acak
return sessions[session_id]["user_id"]
@app.get("/profile/")
async def read_profile(user_id: str = Depends(get_user_id)):
return {"user_id": user_id}
Menguji Dependensi
Salah satu manfaat utama injeksi dependensi adalah peningkatan kemampuan pengujian. Dengan memisahkan komponen, Anda dapat dengan mudah mengganti dependensi dengan mock atau stub selama pengujian.
1. Mengganti Dependensi dalam Pengujian
Seperti yang ditunjukkan sebelumnya, mekanisme dependency_overrides FastAPI sangat ideal untuk pengujian. Buat dependensi mock yang mengembalikan hasil yang dapat diprediksi dan gunakan untuk mengisolasi kode Anda yang sedang diuji.
from fastapi.testclient import TestClient
from fastapi import FastAPI, Depends
app = FastAPI()
# Dependensi asli
def get_external_data():
# Simulasikan pengambilan data dari API eksternal
return {"data": "Data eksternal nyata"}
@app.get("/data/")
async def read_data(data: dict = Depends(get_external_data)):
return data
# Uji
from unittest.mock import MagicMock
def get_external_data_mock():
return {"data": "Data eksternal yang di-mock"}
def test_read_data():
app.dependency_overrides[get_external_data] = get_external_data_mock
client = TestClient(app)
response = client.get("/data/")
assert response.status_code == 200
assert response.json() == {"data": "Data eksternal yang di-mock"}
# Bersihkan timpa
app.dependency_overrides.clear()
2. Menggunakan Pustaka Mocking
Pustaka seperti unittest.mock menyediakan alat yang ampuh untuk membuat objek mock dan mengendalikan perilakunya. Anda dapat menggunakan mock untuk mensimulasikan dependensi yang kompleks dan memverifikasi bahwa kode Anda berinteraksi dengan mereka dengan benar.
import unittest
from unittest.mock import MagicMock
# (Tentukan aplikasi FastAPI dan get_external_data seperti di atas)
class TestReadData(unittest.TestCase):
def test_read_data_with_mock(self):
# Buat mock untuk dependensi get_external_data
mock_get_external_data = MagicMock(return_value={"data": "Data yang di-mock dari unittest"})
# Timpa dependensi dengan mock
app.dependency_overrides[get_external_data] = mock_get_external_data
client = TestClient(app)
response = client.get("/data/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"data": "Data yang di-mock dari unittest"})
# Pastikan mock dipanggil
mock_get_external_data.assert_called_once()
# Bersihkan timpa
app.dependency_overrides.clear()
3. Injeksi Dependensi untuk Pengujian Unit (Di Luar Konteks FastAPI)
Bahkan saat fungsi pengujian unit *di luar* handler endpoint API, prinsip injeksi dependensi tetap berlaku. Alih-alih bergantung pada `Depends` FastAPI, suntikkan dependensi secara manual ke dalam fungsi yang sedang diuji.
# Contoh fungsi untuk diuji
def process_data(data_source):
data = data_source.fetch_data()
# ... proses data ...
return processed_data
class MockDataSource:
def fetch_data(self):
return {"example": "data"}
# Uji unit
def test_process_data():
mock_data_source = MockDataSource()
result = process_data(mock_data_source)
# Assertion pada hasilnya
Pertimbangan Keamanan dengan Injeksi Dependensi
Injeksi dependensi, meskipun bermanfaat, memperkenalkan potensi masalah keamanan jika tidak diterapkan dengan hati-hati.
1. Kebingungan Dependensi
Pastikan Anda menarik dependensi dari sumber tepercaya. Verifikasi integritas paket dan gunakan pengelola paket dengan kemampuan pemindaian kerentanan. Ini adalah prinsip keamanan rantai pasokan perangkat lunak umum, tetapi diperburuk oleh DI karena Anda mungkin menyuntikkan komponen dari berbagai sumber.
2. Injeksi Dependensi Berbahaya
Waspadai dependensi yang menerima input eksternal tanpa validasi yang tepat. Seorang penyerang berpotensi menyuntikkan kode atau data berbahaya melalui dependensi yang disusupi. Sanitasi semua input pengguna dan terapkan mekanisme validasi yang kuat.
3. Kebocoran Informasi melalui Dependensi
Pastikan bahwa dependensi tidak secara tidak sengaja mengekspos informasi sensitif. Tinjau kode dan konfigurasi dependensi Anda untuk mengidentifikasi potensi kerentanan kebocoran informasi.
4. Rahasia yang Dikodekan Secara Keras
Hindari hardcoding rahasia (kunci API, kata sandi database, dll.) langsung ke dalam kode dependensi Anda. Gunakan variabel lingkungan atau alat pengelolaan konfigurasi yang aman untuk menyimpan dan mengelola rahasia.
import os
from fastapi import FastAPI, Depends
app = FastAPI()
def get_api_key():
api_key = os.environ.get("API_KEY")
if not api_key:
raise ValueError("Variabel lingkungan API_KEY tidak diatur.")
return api_key
@app.get("/secure_endpoint/")
async def secure_endpoint(api_key: str = Depends(get_api_key)):
# Gunakan api_key untuk otentikasi/otorisasi
return {"message": "Akses diberikan"}
Optimasi Kinerja dengan Injeksi Dependensi
Injeksi dependensi dapat memengaruhi kinerja jika tidak digunakan dengan bijak. Berikut adalah beberapa strategi optimasi:
1. Minimalkan Biaya Pembuatan Dependensi
Hindari membuat dependensi mahal pada setiap permintaan jika memungkinkan. Jika dependensi tidak berstatus atau dapat dibagikan di seluruh permintaan, pertimbangkan untuk menggunakan cakupan singleton atau menyimpan instance dependensi dalam cache.
2. Inisialisasi Malas
Inisialisasi dependensi hanya saat dibutuhkan. Ini dapat mengurangi waktu startup dan konsumsi memori, terutama untuk aplikasi dengan banyak dependensi.
3. Caching Hasil Dependensi
Cache hasil komputasi dependensi yang mahal jika hasilnya kemungkinan akan digunakan kembali. Gunakan mekanisme caching (misalnya, Redis, Memcached) untuk menyimpan dan mengambil hasil dependensi.
4. Optimalkan Grafik Dependensi
Analisis grafik dependensi Anda untuk mengidentifikasi potensi kemacetan. Sederhanakan struktur dependensi dan kurangi jumlah dependensi jika memungkinkan.
5. Dependensi Asinkron untuk Operasi Terikat I/O
Gunakan dependensi asinkron saat melakukan operasi I/O pemblokiran, seperti kueri database atau panggilan API eksternal. Ini mencegah pemblokiran thread utama dan meningkatkan responsivitas aplikasi secara keseluruhan.
Praktik Terbaik untuk Injeksi Dependensi FastAPI
- Jaga Dependensi Tetap Sederhana: Bertujuan untuk dependensi kecil dan terfokus yang melakukan satu tugas. Ini meningkatkan keterbacaan, kemampuan pengujian, dan pemeliharaan.
- Gunakan Petunjuk Tipe: Manfaatkan petunjuk tipe untuk mendefinisikan dengan jelas tipe input dan output yang diharapkan dari dependensi. Ini meningkatkan kejelasan kode dan memungkinkan FastAPI melakukan pemeriksaan tipe statis.
- Dokumentasikan Dependensi: Dokumentasikan tujuan dan penggunaan setiap dependensi. Ini membantu pengembang lain memahami cara menggunakan dan memelihara kode Anda.
- Uji Dependensi Secara Menyeluruh: Tulis pengujian unit untuk dependensi Anda untuk memastikan mereka berperilaku seperti yang diharapkan. Ini membantu mencegah bug dan meningkatkan keandalan aplikasi Anda secara keseluruhan.
- Gunakan Konvensi Penamaan yang Konsisten: Gunakan konvensi penamaan yang konsisten untuk dependensi Anda untuk meningkatkan keterbacaan kode.
- Hindari Dependensi Melingkar: Dependensi melingkar dapat menyebabkan kode yang kompleks dan sulit di-debug. Refaktor kode Anda untuk menghilangkan dependensi melingkar.
- Pertimbangkan Kontainer Injeksi Dependensi (Opsional): Meskipun injeksi dependensi bawaan FastAPI cukup untuk sebagian besar kasus, pertimbangkan untuk menggunakan kontainer injeksi dependensi khusus (misalnya, `inject`, `autowire`) untuk aplikasi yang lebih kompleks.
Kesimpulan
Sistem injeksi dependensi FastAPI adalah alat ampuh yang mempromosikan modularitas, kemampuan pengujian, dan penggunaan kembali. Dengan menguasai teknik tingkat lanjut, seperti menggunakan kelas sebagai dependensi, mengganti dependensi, dan menggunakan contextvars, Anda dapat membangun API yang kuat dan terukur. Memahami cakupan dan siklus hidup dependensi sangat penting untuk mengelola sumber daya secara efektif. Selalu prioritaskan pengujian dependensi Anda secara menyeluruh untuk memastikan keandalan dan keamanan aplikasi Anda. Dengan mengikuti praktik terbaik dan mempertimbangkan potensi implikasi keamanan dan kinerja, Anda dapat memanfaatkan potensi penuh dari sistem injeksi dependensi FastAPI.